﻿using System;
using System.Collections.Generic;
using System.Linq;
using FarseerGames.FarseerPhysics.Collisions;
using FarseerGames.FarseerPhysics.Dynamics;
using FarseerGames.FarseerPhysics.Mathematics;
using FreeGoo.GameEngine.Renderables.Links;
using FreeGoo.GameEngine.Renderables.Particles.States;

namespace FreeGoo.GameEngine.Renderables.Particles
{
    public abstract class Particle : Linkable
    {
        public const float DefaultParticleLinearDragCoefficient = 50;
        public const float DefaultLinkableDistance = 125;
        public const float DefaultStrongLinkableDistance = 75;
        public const float WakeupDistanceMultiplier = 4;
        public const float SingleLinkLinkUtilization = 0.6f;
        private float _radius;
        private float _radiusSquared;
        private ParticleState _state;

        public Particle(Geom geom, float radius)
            : base(geom)
        {
            Radius = radius;
            LinkableDistance = DefaultLinkableDistance;
            MaxLinkSum = LinkableDistance * 2;
            MinimumLinkTargets = 2;
            MaximumLinkTargets = 5;
            CanBeUnlinked = true;
            LinkableDistanceLinkBias = DefaultLinkableDistanceLinkBias;            
            geom.Body.MomentOfInertia = float.PositiveInfinity;
            State = new FreeFall(this);
            Body.LinearDragCoefficient = DefaultParticleLinearDragCoefficient;
            NeverShortenWantedDistance = true;
        }

        public override float Radius
        {
            get
            {
                return _radius;
            }

            set
            {
                _radius = value;
                _radiusSquared = value * value;
            }
        }

        public float RadiusSquared { get { return _radiusSquared; } }
        public float LinkableDistance { get; protected set; }
        public float MaxLinkSum { get; protected set; }
        public int MinimumLinkTargets { get; set; }
        public bool CanBeUnlinked { get; protected set; }
        public int MaximumLinkTargets { get; set; }
        public float LinkableDistanceLinkBias { get; set; }
        public bool NeverShortenWantedDistance { get; set; }
        public bool CanBeSelected
        {
            get
            {
                return
                    (Links.Count == 0 || CanBeUnlinked) && State.ParticleSelectableInState;
            }
        }

        public ParticleState State
        {
            get
            {
                return _state;
            }

            set
            {
                if (_state != null)
                {
                    _state.ExitingState();
                }

                _state = value;
                if (_state != null)
                {
                    _state.EnteringState();
                }

                SignalStateChanged();
            }
        }

        public bool IsSleeping
        {
            get
            {
                return State is Sleeping;
            }

            set
            {
                if (IsSleeping != value)
                {
                    if (value)
                    {
                        // Time to wake up
                        State = new Sleeping(this);
                    }
                    else
                    {
                        // Time to wake up
                        State = new FreeFall(this);
                    }
                }
            }
        }

        public bool IsSelected
        {
            get
            {
                return Engine.UserInputController.SelectedParticle == this && Engine.UserInputController.IsMouseDown;
            }
        }

        public override void UpdatePhysics()
        {
            base.UpdatePhysics();
            if (State != null)
            {
                State.PerformStatePhysicsAction();
            }
        }

        public override void Selected(Vector2 cursorPosition)
        {
            // Clear all links!
            if (CanBeSelected)
            {
                State = new Selected(this);
            }
        }

        public override void DeSelected()
        {
            State = new FreeFall(this);
            SignalStateChanged();
        }

        public void Placed()
        {
            if (ScheduledForDelete)
            {
                return;
            }

            ParticleConnectionTestResult result = ParticleConnectionTest();

            if (result.ResultType == ParticleConnectionTestResultType.ListOfLinkedParticles)
            {
                foreach (ILinkable linkable in result.WouldConnectTo)
                {
                    CreateLinkTo(linkable);
                }
            }
            else if (result.ResultType == ParticleConnectionTestResultType.LinkTwoOtherParticles)
            {
                CreateLinkBetween(result.FirstLinkable, result.SecondLinkable);
            }
            else
            {
                // Placement failed, so it's in freefall again
                State = new FreeFall(this);
            }
        }

        public bool GetPointIsInParticle(Vector2 vector2, out float distanceSquared)
        {
            distanceSquared = Vector2.DistanceSquared(vector2, Body.Position);
            return distanceSquared < RadiusSquared;
        }

        public bool GetPointIsInParticle(Vector2 vector2)
        {
            float distanceSquared;
            return GetPointIsInParticle(vector2, out distanceSquared);
        }

        public override void AddToEngine(Engine engine)
        {
            base.AddToEngine(engine);
            Engine.Particles.Add(this);
        }

        public override void Delete()
        {
            base.Delete();
            Engine.Particles.Remove(this);
        }

        // TODO: Serious refactoring required!
        public ParticleConnectionTestResult ParticleConnectionTest()
        {
            ParticleConnectionTestResult result = new ParticleConnectionTestResult();

            List<ILinkable> linkables = FilterLinkables(Engine.GetLinkablesNearPosition(Body.Position, LinkableDistance));

            if (TooCloseToOtherParticle(linkables))
            {
                result.ResultType = ParticleConnectionTestResultType.NothingToDo;
                return result;
            }

            float sum = 0;
            linkables =
                linkables
                .Where(particle => (sum += particle.LastTestDistance) < MaxLinkSum)
                .Take(MaximumLinkTargets)
                .ToList();

            foreach (ILinkable firstLinkable in linkables)
            {
                foreach (ILinkable secondLinkable in linkables)
                {
                    if (firstLinkable == secondLinkable)
                    {
                        continue;
                    }

                    Vector2 center = (firstLinkable.Position + secondLinkable.Position) * 0.5f;

                    if (GetPointIsInParticle(center))
                    {
                        if (!firstLinkable.IsLinkedTo(secondLinkable) && Vector2.Distance(firstLinkable.Position, secondLinkable.Position) < SingleLinkLinkUtilization * MaxLinkSum)
                        {
                            result.ResultType = ParticleConnectionTestResultType.LinkTwoOtherParticles;
                            result.FirstLinkable = firstLinkable;
                            result.SecondLinkable = secondLinkable;
                        }
                        else
                        {
                            result.ResultType = ParticleConnectionTestResultType.NothingToDo;
                        }

                        return result;
                    }
                }
            }

            // Can't connect to two particles that aren't allready connected to another particle in the
            // group! We might need to break this rule for future particles
            if (MinimumLinkTargets > 1)
            {
                RemoveLinkablesThatArentConnected(linkables);
            }

            if (linkables.Count < MinimumLinkTargets)
            {
                result.ResultType = ParticleConnectionTestResultType.NothingToDo;
            }
            else
            {
                result.WouldConnectTo = linkables;
                result.ResultType = ParticleConnectionTestResultType.ListOfLinkedParticles;
            }

            return result;
        }

        public override Link CreateLinkBetween(ILinkable first, ILinkable second)
        {
            StopParticle();
            Link link = base.CreateLinkBetween(first, second);
            link.WantedTargetDistance = (LinkableDistanceLinkBias * LinkableDistance + link.TargetDistance * (1 - LinkableDistanceLinkBias));
            if (NeverShortenWantedDistance)
            {
                link.WantedTargetDistance = Math.Max(link.TargetDistance, (float)link.WantedTargetDistance);
            }

            if (this != first)
            {
                State = new AbsorbedByLink(this, first, second, link);
            }
            else
            {
                State = new Linked(this);
            }

            return link;
        }

        public void ClearRenderer()
        {
            if (Renderer != null)
            {
                Renderer.Delete();
                Renderer = null;
            }
        }

        public void SetRenderer()
        {
            ClearRenderer();
            Engine.RendererFactory.SetRenderer(this);
        }

        internal void ControllerMovedTo(Vector2 cursorPosition)
        {
            if (State is Selected)
            {
                (State as Selected).ControllerMovedTo(cursorPosition);
            }
        }

        private static void RemoveLinkablesThatArentConnected(List<ILinkable> linkables)
        {
            List<ILinkable> removeLinkables = new List<ILinkable>();
            foreach (ILinkable linkable in linkables)
            {
                bool keep = false;
                foreach (ILinkable secondlinkable in linkables)
                {
                    if (linkable == secondlinkable)
                    {
                        continue;
                    }

                    if (linkable.IsLinkedTo(secondlinkable))
                    {
                        keep = true;
                    }
                }

                if (!keep)
                {
                    removeLinkables.Add(linkable);
                }
            }

            linkables.RemoveAll(removeLinkables.Contains);
        }

        private bool TooCloseToOtherParticle(IEnumerable<ILinkable> linkables)
        {
            foreach (ILinkable linkable in linkables)
            {
                if (linkable.LastTestDistance < (Radius + linkable.Radius) * MinAllowedLinkDistance)
                {
                    return true;
                }
            }

            return false;
        }

        private void StopParticle()
        {
            Body.AngularVelocity = 0;
            Body.LinearVelocity = Vector2.Zero;
        }

        private List<ILinkable> FilterLinkables(IEnumerable<ILinkable> linkables)
        {
            List<ILinkable> filtered = new List<ILinkable>();

            foreach (ILinkable linkable in linkables)
            {
                if (AllowsLinkTo(linkable))
                {
                    filtered.Add(linkable);
                }
            }

            return filtered;
        }
    }
}
